/*
 * Copyright (c) 2002, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package com.sun.java.swing.plaf.gtk;

import sun.swing.SwingUtilities2;
import com.sun.java.swing.plaf.gtk.GTKConstants.ArrowType;
import com.sun.java.swing.plaf.gtk.GTKConstants.ShadowType;

import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.synth.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;

import javax.xml.parsers.*;
import org.xml.sax.SAXException;
import org.w3c.dom.*;

/**
 */
class Metacity implements SynthConstants {
  // Tutorial:
  // http://developer.gnome.org/doc/tutorials/metacity/metacity-themes.html

  // Themes:
  // http://art.gnome.org/theme_list.php?category=metacity

  static Metacity INSTANCE;

  private static final String[] themeNames = {
      getUserTheme(),
      "blueprint",
      "Bluecurve",
      "Crux",
      "SwingFallbackTheme"
  };

  static {
    for (String themeName : themeNames) {
      if (themeName != null) {
        try {
          INSTANCE = new Metacity(themeName);
        } catch (FileNotFoundException ex) {
        } catch (IOException ex) {
          logError(themeName, ex);
        } catch (ParserConfigurationException ex) {
          logError(themeName, ex);
        } catch (SAXException ex) {
          logError(themeName, ex);
        }
      }
      if (INSTANCE != null) {
        break;
      }
    }
    if (INSTANCE == null) {
      throw new Error("Could not find any installed metacity theme, and fallback failed");
    }
  }

  private static boolean errorLogged = false;
  private static DocumentBuilder documentBuilder;
  private static Document xmlDoc;
  private static String userHome;

  private Node frame_style_set;
  private Map<String, Object> frameGeometry;
  private Map<String, Map<String, Object>> frameGeometries;

  private LayoutManager titlePaneLayout = new TitlePaneLayout();

  private ColorizeImageFilter imageFilter = new ColorizeImageFilter();
  private URL themeDir = null;
  private SynthContext context;
  private String themeName;

  private ArithmeticExpressionEvaluator aee = new ArithmeticExpressionEvaluator();
  private Map<String, Integer> variables;

  // Reusable clip shape object
  private RoundRectClipShape roundedClipShape;

  protected Metacity(String themeName)
      throws IOException, ParserConfigurationException, SAXException {
    this.themeName = themeName;
    themeDir = getThemeDir(themeName);
    if (themeDir != null) {
      URL themeURL = new URL(themeDir, "metacity-theme-1.xml");
      xmlDoc = getXMLDoc(themeURL);
      if (xmlDoc == null) {
        throw new IOException(themeURL.toString());
      }
    } else {
      throw new FileNotFoundException(themeName);
    }

    // Initialize constants
    variables = new HashMap<String, Integer>();
    NodeList nodes = xmlDoc.getElementsByTagName("constant");
    int n = nodes.getLength();
    for (int i = 0; i < n; i++) {
      Node node = nodes.item(i);
      String name = getStringAttr(node, "name");
      if (name != null) {
        String value = getStringAttr(node, "value");
        if (value != null) {
          try {
            variables.put(name, Integer.parseInt(value));
          } catch (NumberFormatException ex) {
            logError(themeName, ex);
            // Ignore bad value
          }
        }
      }
    }

    // Cache frame geometries
    frameGeometries = new HashMap<String, Map<String, Object>>();
    nodes = xmlDoc.getElementsByTagName("frame_geometry");
    n = nodes.getLength();
    for (int i = 0; i < n; i++) {
      Node node = nodes.item(i);
      String name = getStringAttr(node, "name");
      if (name != null) {
        HashMap<String, Object> gm = new HashMap<String, Object>();
        frameGeometries.put(name, gm);

        String parentGM = getStringAttr(node, "parent");
        if (parentGM != null) {
          gm.putAll(frameGeometries.get(parentGM));
        }

        gm.put("has_title",
            Boolean.valueOf(getBooleanAttr(node, "has_title", true)));
        gm.put("rounded_top_left",
            Boolean.valueOf(getBooleanAttr(node, "rounded_top_left", false)));
        gm.put("rounded_top_right",
            Boolean.valueOf(getBooleanAttr(node, "rounded_top_right", false)));
        gm.put("rounded_bottom_left",
            Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_left", false)));
        gm.put("rounded_bottom_right",
            Boolean.valueOf(getBooleanAttr(node, "rounded_bottom_right", false)));

        NodeList childNodes = node.getChildNodes();
        int nc = childNodes.getLength();
        for (int j = 0; j < nc; j++) {
          Node child = childNodes.item(j);
          if (child.getNodeType() == Node.ELEMENT_NODE) {
            name = child.getNodeName();
            Object value = null;
            if ("distance".equals(name)) {
              value = Integer.valueOf(getIntAttr(child, "value", 0));
            } else if ("border".equals(name)) {
              value = new Insets(getIntAttr(child, "top", 0),
                  getIntAttr(child, "left", 0),
                  getIntAttr(child, "bottom", 0),
                  getIntAttr(child, "right", 0));
            } else if ("aspect_ratio".equals(name)) {
              value = new Float(getFloatAttr(child, "value", 1.0F));
            } else {
              logError(themeName, "Unknown Metacity frame geometry value type: " + name);
            }
            String childName = getStringAttr(child, "name");
            if (childName != null && value != null) {
              gm.put(childName, value);
            }
          }
        }
      }
    }
    frameGeometry = frameGeometries.get("normal");
  }


  public static LayoutManager getTitlePaneLayout() {
    return INSTANCE.titlePaneLayout;
  }

  private Shape getRoundedClipShape(int x, int y, int w, int h,
      int arcw, int arch, int corners) {
    if (roundedClipShape == null) {
      roundedClipShape = new RoundRectClipShape();
    }
    roundedClipShape.setRoundedRect(x, y, w, h, arcw, arch, corners);

    return roundedClipShape;
  }

  void paintButtonBackground(SynthContext context, Graphics g, int x, int y, int w, int h) {
    updateFrameGeometry(context);

    this.context = context;
    JButton button = (JButton) context.getComponent();
    String buttonName = button.getName();
    int buttonState = context.getComponentState();

    JComponent titlePane = (JComponent) button.getParent();
    Container titlePaneParent = titlePane.getParent();

    JInternalFrame jif;
    if (titlePaneParent instanceof JInternalFrame) {
      jif = (JInternalFrame) titlePaneParent;
    } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
      jif = ((JInternalFrame.JDesktopIcon) titlePaneParent).getInternalFrame();
    } else {
      return;
    }

    boolean active = jif.isSelected();
    button.setOpaque(false);

    String state = "normal";
    if ((buttonState & PRESSED) != 0) {
      state = "pressed";
    } else if ((buttonState & MOUSE_OVER) != 0) {
      state = "prelight";
    }

    String function = null;
    String location = null;
    boolean left_corner = false;
    boolean right_corner = false;

    if (buttonName == "InternalFrameTitlePane.menuButton") {
      function = "menu";
      location = "left_left";
      left_corner = true;
    } else if (buttonName == "InternalFrameTitlePane.iconifyButton") {
      function = "minimize";
      int nButtons = ((jif.isIconifiable() ? 1 : 0) +
          (jif.isMaximizable() ? 1 : 0) +
          (jif.isClosable() ? 1 : 0));
      right_corner = (nButtons == 1);
      switch (nButtons) {
        case 1:
          location = "right_right";
          break;
        case 2:
          location = "right_middle";
          break;
        case 3:
          location = "right_left";
          break;
      }
    } else if (buttonName == "InternalFrameTitlePane.maximizeButton") {
      function = "maximize";
      right_corner = !jif.isClosable();
      location = jif.isClosable() ? "right_middle" : "right_right";
    } else if (buttonName == "InternalFrameTitlePane.closeButton") {
      function = "close";
      right_corner = true;
      location = "right_right";
    }

    Node frame = getNode(frame_style_set, "frame", new String[]{
        "focus", (active ? "yes" : "no"),
        "state", (jif.isMaximum() ? "maximized" : "normal")
    });

    if (function != null && frame != null) {
      Node frame_style = getNode("frame_style", new String[]{
          "name", getStringAttr(frame, "style")
      });
      if (frame_style != null) {
        Shape oldClip = g.getClip();
        if ((right_corner && getBoolean("rounded_top_right", false)) ||
            (left_corner && getBoolean("rounded_top_left", false))) {

          Point buttonLoc = button.getLocation();
          if (right_corner) {
            g.setClip(getRoundedClipShape(0, 0, w, h,
                12, 12, RoundRectClipShape.TOP_RIGHT));
          } else {
            g.setClip(getRoundedClipShape(0, 0, w, h,
                11, 11, RoundRectClipShape.TOP_LEFT));
          }

          Rectangle clipBounds = oldClip.getBounds();
          g.clipRect(clipBounds.x, clipBounds.y,
              clipBounds.width, clipBounds.height);
        }
        drawButton(frame_style, location + "_background", state, g, w, h, jif);
        drawButton(frame_style, function, state, g, w, h, jif);
        g.setClip(oldClip);
      }
    }
  }

  protected void drawButton(Node frame_style, String function, String state,
      Graphics g, int w, int h, JInternalFrame jif) {
    Node buttonNode = getNode(frame_style, "button",
        new String[]{"function", function, "state", state});
    if (buttonNode == null && !state.equals("normal")) {
      buttonNode = getNode(frame_style, "button",
          new String[]{"function", function, "state", "normal"});
    }
    if (buttonNode != null) {
      Node draw_ops;
      String draw_ops_name = getStringAttr(buttonNode, "draw_ops");
      if (draw_ops_name != null) {
        draw_ops = getNode("draw_ops", new String[]{"name", draw_ops_name});
      } else {
        draw_ops = getNode(buttonNode, "draw_ops", null);
      }
      variables.put("width", w);
      variables.put("height", h);
      draw(draw_ops, g, jif);
    }
  }

  void paintFrameBorder(SynthContext context, Graphics g, int x0, int y0, int width, int height) {
    updateFrameGeometry(context);

    this.context = context;
    JComponent comp = context.getComponent();
    JComponent titlePane = findChild(comp, "InternalFrame.northPane");

    if (titlePane == null) {
      return;
    }

    JInternalFrame jif = null;
    if (comp instanceof JInternalFrame) {
      jif = (JInternalFrame) comp;
    } else if (comp instanceof JInternalFrame.JDesktopIcon) {
      jif = ((JInternalFrame.JDesktopIcon) comp).getInternalFrame();
    } else {
      assert false : "component is not JInternalFrame or JInternalFrame.JDesktopIcon";
      return;
    }

    boolean active = jif.isSelected();
    Font oldFont = g.getFont();
    g.setFont(titlePane.getFont());
    g.translate(x0, y0);

    Rectangle titleRect = calculateTitleArea(jif);
    JComponent menuButton = findChild(titlePane, "InternalFrameTitlePane.menuButton");

    Icon frameIcon = jif.getFrameIcon();
    variables.put("mini_icon_width",
        (frameIcon != null) ? frameIcon.getIconWidth() : 0);
    variables.put("mini_icon_height",
        (frameIcon != null) ? frameIcon.getIconHeight() : 0);
    variables.put("title_width", calculateTitleTextWidth(g, jif));
    FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
    variables.put("title_height", fm.getAscent() + fm.getDescent());

    // These don't seem to apply here, but the Galaxy theme uses them. Not sure why.
    variables.put("icon_width", 32);
    variables.put("icon_height", 32);

    if (frame_style_set != null) {
      Node frame = getNode(frame_style_set, "frame", new String[]{
          "focus", (active ? "yes" : "no"),
          "state", (jif.isMaximum() ? "maximized" : "normal")
      });

      if (frame != null) {
        Node frame_style = getNode("frame_style", new String[]{
            "name", getStringAttr(frame, "style")
        });
        if (frame_style != null) {
          Shape oldClip = g.getClip();
          boolean roundTopLeft = getBoolean("rounded_top_left", false);
          boolean roundTopRight = getBoolean("rounded_top_right", false);
          boolean roundBottomLeft = getBoolean("rounded_bottom_left", false);
          boolean roundBottomRight = getBoolean("rounded_bottom_right", false);

          if (roundTopLeft || roundTopRight || roundBottomLeft || roundBottomRight) {
            jif.setOpaque(false);

            g.setClip(getRoundedClipShape(0, 0, width, height, 12, 12,
                (roundTopLeft ? RoundRectClipShape.TOP_LEFT : 0) |
                    (roundTopRight ? RoundRectClipShape.TOP_RIGHT : 0) |
                    (roundBottomLeft ? RoundRectClipShape.BOTTOM_LEFT : 0) |
                    (roundBottomRight ? RoundRectClipShape.BOTTOM_RIGHT : 0)));
          }

          Rectangle clipBounds = oldClip.getBounds();
          g.clipRect(clipBounds.x, clipBounds.y,
              clipBounds.width, clipBounds.height);

          int titleHeight = titlePane.getHeight();

          boolean minimized = jif.isIcon();
          Insets insets = getBorderInsets(context, null);

          int leftTitlebarEdge = getInt("left_titlebar_edge");
          int rightTitlebarEdge = getInt("right_titlebar_edge");
          int topTitlebarEdge = getInt("top_titlebar_edge");
          int bottomTitlebarEdge = getInt("bottom_titlebar_edge");

          if (!minimized) {
            drawPiece(frame_style, g, "entire_background",
                0, 0, width, height, jif);
          }
          drawPiece(frame_style, g, "titlebar",
              0, 0, width, titleHeight, jif);
          drawPiece(frame_style, g, "titlebar_middle",
              leftTitlebarEdge, topTitlebarEdge,
              width - leftTitlebarEdge - rightTitlebarEdge,
              titleHeight - topTitlebarEdge - bottomTitlebarEdge,
              jif);
          drawPiece(frame_style, g, "left_titlebar_edge",
              0, 0, leftTitlebarEdge, titleHeight, jif);
          drawPiece(frame_style, g, "right_titlebar_edge",
              width - rightTitlebarEdge, 0,
              rightTitlebarEdge, titleHeight, jif);
          drawPiece(frame_style, g, "top_titlebar_edge",
              0, 0, width, topTitlebarEdge, jif);
          drawPiece(frame_style, g, "bottom_titlebar_edge",
              0, titleHeight - bottomTitlebarEdge,
              width, bottomTitlebarEdge, jif);
          drawPiece(frame_style, g, "title",
              titleRect.x, titleRect.y, titleRect.width, titleRect.height, jif);
          if (!minimized) {
            drawPiece(frame_style, g, "left_edge",
                0, titleHeight, insets.left, height - titleHeight, jif);
            drawPiece(frame_style, g, "right_edge",
                width - insets.right, titleHeight, insets.right, height - titleHeight, jif);
            drawPiece(frame_style, g, "bottom_edge",
                0, height - insets.bottom, width, insets.bottom, jif);
            drawPiece(frame_style, g, "overlay",
                0, 0, width, height, jif);
          }
          g.setClip(oldClip);
        }
      }
    }
    g.translate(-x0, -y0);
    g.setFont(oldFont);
  }


  private static class Privileged implements PrivilegedAction<Object> {

    private static int GET_THEME_DIR = 0;
    private static int GET_USER_THEME = 1;
    private static int GET_IMAGE = 2;
    private int type;
    private Object arg;

    public Object doPrivileged(int type, Object arg) {
      this.type = type;
      this.arg = arg;
      return AccessController.doPrivileged(this);
    }

    public Object run() {
      if (type == GET_THEME_DIR) {
        String sep = File.separator;
        String[] dirs = new String[]{
            userHome + sep + ".themes",
            System.getProperty("swing.metacitythemedir"),
            "/usr/X11R6/share/themes",
            "/usr/X11R6/share/gnome/themes",
            "/usr/local/share/themes",
            "/usr/local/share/gnome/themes",
            "/usr/share/themes",
            "/usr/gnome/share/themes",  // Debian/Redhat/Solaris
            "/opt/gnome2/share/themes"  // SuSE
        };

        URL themeDir = null;
        for (int i = 0; i < dirs.length; i++) {
          // System property may not be set so skip null directories.
          if (dirs[i] == null) {
            continue;
          }
          File dir =
              new File(dirs[i] + sep + arg + sep + "metacity-1");
          if (new File(dir, "metacity-theme-1.xml").canRead()) {
            try {
              themeDir = dir.toURI().toURL();
            } catch (MalformedURLException ex) {
              themeDir = null;
            }
            break;
          }
        }
        if (themeDir == null) {
          String filename = "resources/metacity/" + arg +
              "/metacity-1/metacity-theme-1.xml";
          URL url = getClass().getResource(filename);
          if (url != null) {
            String str = url.toString();
            try {
              themeDir = new URL(str.substring(0, str.lastIndexOf('/')) + "/");
            } catch (MalformedURLException ex) {
              themeDir = null;
            }
          }
        }
        return themeDir;
      } else if (type == GET_USER_THEME) {
        try {
          // Set userHome here because we need the privilege
          userHome = System.getProperty("user.home");

          String theme = System.getProperty("swing.metacitythemename");
          if (theme != null) {
            return theme;
          }
          // Note: this is a small file (< 1024 bytes) so it's not worth
          // starting an XML parser or even to use a buffered reader.
          URL url = new URL(new File(userHome).toURI().toURL(),
              ".gconf/apps/metacity/general/%25gconf.xml");
          // Pending: verify character encoding spec for gconf
          Reader reader = new InputStreamReader(url.openStream(), "ISO-8859-1");
          char[] buf = new char[1024];
          StringBuffer strBuf = new StringBuffer();
          int n;
          while ((n = reader.read(buf)) >= 0) {
            strBuf.append(buf, 0, n);
          }
          reader.close();
          String str = strBuf.toString();
          if (str != null) {
            String strLowerCase = str.toLowerCase();
            int i = strLowerCase.indexOf("<entry name=\"theme\"");
            if (i >= 0) {
              i = strLowerCase.indexOf("<stringvalue>", i);
              if (i > 0) {
                i += "<stringvalue>".length();
                int i2 = str.indexOf("<", i);
                return str.substring(i, i2);
              }
            }
          }
        } catch (MalformedURLException ex) {
          // OK to just ignore. We'll use a fallback theme.
        } catch (IOException ex) {
          // OK to just ignore. We'll use a fallback theme.
        }
        return null;
      } else if (type == GET_IMAGE) {
        return new ImageIcon((URL) arg).getImage();
      } else {
        return null;
      }
    }
  }

  private static URL getThemeDir(String themeName) {
    return (URL) new Privileged().doPrivileged(Privileged.GET_THEME_DIR, themeName);
  }

  private static String getUserTheme() {
    return (String) new Privileged().doPrivileged(Privileged.GET_USER_THEME, null);
  }

  protected void tileImage(Graphics g, Image image, int x0, int y0, int w, int h, float[] alphas) {
    Graphics2D g2 = (Graphics2D) g;
    Composite oldComp = g2.getComposite();

    int sw = image.getWidth(null);
    int sh = image.getHeight(null);
    int y = y0;
    while (y < y0 + h) {
      sh = Math.min(sh, y0 + h - y);
      int x = x0;
      while (x < x0 + w) {
        float f = (alphas.length - 1.0F) * x / (x0 + w);
        int i = (int) f;
        f -= (int) f;
        float alpha = (1 - f) * alphas[i];
        if (i + 1 < alphas.length) {
          alpha += f * alphas[i + 1];
        }
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
        int swm = Math.min(sw, x0 + w - x);
        g.drawImage(image, x, y, x + swm, y + sh, 0, 0, swm, sh, null);
        x += swm;
      }
      y += sh;
    }
    g2.setComposite(oldComp);
  }

  private HashMap<String, Image> images = new HashMap<String, Image>();

  protected Image getImage(String key, Color c) {
    Image image = images.get(key + "-" + c.getRGB());
    if (image == null) {
      image = imageFilter.colorize(getImage(key), c);
      if (image != null) {
        images.put(key + "-" + c.getRGB(), image);
      }
    }
    return image;
  }

  protected Image getImage(String key) {
    Image image = images.get(key);
    if (image == null) {
      if (themeDir != null) {
        try {
          URL url = new URL(themeDir, key);
          image = (Image) new Privileged().doPrivileged(Privileged.GET_IMAGE, url);
        } catch (MalformedURLException ex) {
          //log("Bad image url: "+ themeDir + "/" + key);
        }
      }
      if (image != null) {
        images.put(key, image);
      }
    }
    return image;
  }

  private class ColorizeImageFilter extends RGBImageFilter {

    double cr, cg, cb;

    public ColorizeImageFilter() {
      canFilterIndexColorModel = true;
    }

    public void setColor(Color color) {
      cr = color.getRed() / 255.0;
      cg = color.getGreen() / 255.0;
      cb = color.getBlue() / 255.0;
    }

    public Image colorize(Image fromImage, Color c) {
      setColor(c);
      ImageProducer producer = new FilteredImageSource(fromImage.getSource(), this);
      return new ImageIcon(context.getComponent().createImage(producer)).getImage();
    }

    public int filterRGB(int x, int y, int rgb) {
      // Assume all rgb values are shades of gray
      double grayLevel = 2 * (rgb & 0xff) / 255.0;
      double r, g, b;

      if (grayLevel <= 1.0) {
        r = cr * grayLevel;
        g = cg * grayLevel;
        b = cb * grayLevel;
      } else {
        grayLevel -= 1.0;
        r = cr + (1.0 - cr) * grayLevel;
        g = cg + (1.0 - cg) * grayLevel;
        b = cb + (1.0 - cb) * grayLevel;
      }

      return ((rgb & 0xff000000) +
          (((int) (r * 255)) << 16) +
          (((int) (g * 255)) << 8) +
          (int) (b * 255));
    }
  }

  protected static JComponent findChild(JComponent parent, String name) {
    int n = parent.getComponentCount();
    for (int i = 0; i < n; i++) {
      JComponent c = (JComponent) parent.getComponent(i);
      if (name.equals(c.getName())) {
        return c;
      }
    }
    return null;
  }


  protected class TitlePaneLayout implements LayoutManager {

    public void addLayoutComponent(String name, Component c) {
    }

    public void removeLayoutComponent(Component c) {
    }

    public Dimension preferredLayoutSize(Container c) {
      return minimumLayoutSize(c);
    }

    public Dimension minimumLayoutSize(Container c) {
      JComponent titlePane = (JComponent) c;
      Container titlePaneParent = titlePane.getParent();
      JInternalFrame frame;
      if (titlePaneParent instanceof JInternalFrame) {
        frame = (JInternalFrame) titlePaneParent;
      } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
        frame = ((JInternalFrame.JDesktopIcon) titlePaneParent).getInternalFrame();
      } else {
        return null;
      }

      Dimension buttonDim = calculateButtonSize(titlePane);
      Insets title_border = (Insets) getFrameGeometry().get("title_border");
      Insets button_border = (Insets) getFrameGeometry().get("button_border");

      // Calculate width.
      int width = getInt("left_titlebar_edge") + buttonDim.width + getInt("right_titlebar_edge");
      if (title_border != null) {
        width += title_border.left + title_border.right;
      }
      if (frame.isClosable()) {
        width += buttonDim.width;
      }
      if (frame.isMaximizable()) {
        width += buttonDim.width;
      }
      if (frame.isIconifiable()) {
        width += buttonDim.width;
      }
      FontMetrics fm = frame.getFontMetrics(titlePane.getFont());
      String frameTitle = frame.getTitle();
      int title_w = frameTitle != null ? SwingUtilities2.stringWidth(
          frame, fm, frameTitle) : 0;
      int title_length = frameTitle != null ? frameTitle.length() : 0;

      // Leave room for three characters in the title.
      if (title_length > 3) {
        int subtitle_w = SwingUtilities2.stringWidth(
            frame, fm, frameTitle.substring(0, 3) + "...");
        width += (title_w < subtitle_w) ? title_w : subtitle_w;
      } else {
        width += title_w;
      }

      // Calculate height.
      int titleHeight = fm.getHeight() + getInt("title_vertical_pad");
      if (title_border != null) {
        titleHeight += title_border.top + title_border.bottom;
      }
      int buttonHeight = buttonDim.height;
      if (button_border != null) {
        buttonHeight += button_border.top + button_border.bottom;
      }
      int height = Math.max(buttonHeight, titleHeight);

      return new Dimension(width, height);
    }

    public void layoutContainer(Container c) {
      JComponent titlePane = (JComponent) c;
      Container titlePaneParent = titlePane.getParent();
      JInternalFrame frame;
      if (titlePaneParent instanceof JInternalFrame) {
        frame = (JInternalFrame) titlePaneParent;
      } else if (titlePaneParent instanceof JInternalFrame.JDesktopIcon) {
        frame = ((JInternalFrame.JDesktopIcon) titlePaneParent).getInternalFrame();
      } else {
        return;
      }
      Map gm = getFrameGeometry();

      int w = titlePane.getWidth();
      int h = titlePane.getHeight();

      JComponent menuButton = findChild(titlePane, "InternalFrameTitlePane.menuButton");
      JComponent minimizeButton = findChild(titlePane, "InternalFrameTitlePane.iconifyButton");
      JComponent maximizeButton = findChild(titlePane, "InternalFrameTitlePane.maximizeButton");
      JComponent closeButton = findChild(titlePane, "InternalFrameTitlePane.closeButton");

      Insets button_border = (Insets) gm.get("button_border");
      Dimension buttonDim = calculateButtonSize(titlePane);

      int y = (button_border != null) ? button_border.top : 0;
      if (titlePaneParent.getComponentOrientation().isLeftToRight()) {
        int x = getInt("left_titlebar_edge");

        menuButton.setBounds(x, y, buttonDim.width, buttonDim.height);

        x = w - buttonDim.width - getInt("right_titlebar_edge");
        if (button_border != null) {
          x -= button_border.right;
        }

        if (frame.isClosable()) {
          closeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
          x -= buttonDim.width;
        }

        if (frame.isMaximizable()) {
          maximizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
          x -= buttonDim.width;
        }

        if (frame.isIconifiable()) {
          minimizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
        }
      } else {
        int x = w - buttonDim.width - getInt("right_titlebar_edge");

        menuButton.setBounds(x, y, buttonDim.width, buttonDim.height);

        x = getInt("left_titlebar_edge");
        if (button_border != null) {
          x += button_border.left;
        }

        if (frame.isClosable()) {
          closeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
          x += buttonDim.width;
        }

        if (frame.isMaximizable()) {
          maximizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
          x += buttonDim.width;
        }

        if (frame.isIconifiable()) {
          minimizeButton.setBounds(x, y, buttonDim.width, buttonDim.height);
        }
      }
    }
  } // end TitlePaneLayout

  protected Map getFrameGeometry() {
    return frameGeometry;
  }

  protected void setFrameGeometry(JComponent titlePane, Map gm) {
    this.frameGeometry = gm;
    if (getInt("top_height") == 0 && titlePane != null) {
      gm.put("top_height", Integer.valueOf(titlePane.getHeight()));
    }
  }

  protected int getInt(String key) {
    Integer i = (Integer) frameGeometry.get(key);
    if (i == null) {
      i = variables.get(key);
    }
    return (i != null) ? i.intValue() : 0;
  }

  protected boolean getBoolean(String key, boolean fallback) {
    Boolean b = (Boolean) frameGeometry.get(key);
    return (b != null) ? b.booleanValue() : fallback;
  }


  protected void drawArc(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    Color color = parseColor(getStringAttr(attrs, "color"));
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    int start_angle = aee.evaluate(getStringAttr(attrs, "start_angle"));
    int extent_angle = aee.evaluate(getStringAttr(attrs, "extent_angle"));
    boolean filled = getBooleanAttr(node, "filled", false);
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }
    g.setColor(color);
    if (filled) {
      g.fillArc(x, y, w, h, start_angle, extent_angle);
    } else {
      g.drawArc(x, y, w, h, start_angle, extent_angle);
    }
  }

  protected void drawLine(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    Color color = parseColor(getStringAttr(attrs, "color"));
    int x1 = aee.evaluate(getStringAttr(attrs, "x1"));
    int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
    int x2 = aee.evaluate(getStringAttr(attrs, "x2"));
    int y2 = aee.evaluate(getStringAttr(attrs, "y2"));
    int lineWidth = aee.evaluate(getStringAttr(attrs, "width"), 1);
    g.setColor(color);
    if (lineWidth != 1) {
      Graphics2D g2d = (Graphics2D) g;
      Stroke stroke = g2d.getStroke();
      g2d.setStroke(new BasicStroke((float) lineWidth));
      g2d.drawLine(x1, y1, x2, y2);
      g2d.setStroke(stroke);
    } else {
      g.drawLine(x1, y1, x2, y2);
    }
  }

  protected void drawRectangle(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    Color color = parseColor(getStringAttr(attrs, "color"));
    boolean filled = getBooleanAttr(node, "filled", false);
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    g.setColor(color);
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }
    if (filled) {
      g.fillRect(x, y, w, h);
    } else {
      g.drawRect(x, y, w, h);
    }
  }

  protected void drawTile(Node node, Graphics g, JInternalFrame jif) {
    NamedNodeMap attrs = node.getAttributes();
    int x0 = aee.evaluate(getStringAttr(attrs, "x"));
    int y0 = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    int tw = aee.evaluate(getStringAttr(attrs, "tile_width"));
    int th = aee.evaluate(getStringAttr(attrs, "tile_height"));
    int width = getInt("width");
    int height = getInt("height");
    if (width == -1) {
      x0 -= w;
    }
    if (height == -1) {
      y0 -= h;
    }
    Shape oldClip = g.getClip();
    if (g instanceof Graphics2D) {
      ((Graphics2D) g).clip(new Rectangle(x0, y0, w, h));
    }
    variables.put("width", tw);
    variables.put("height", th);

    Node draw_ops = getNode("draw_ops", new String[]{"name", getStringAttr(node, "name")});

    int y = y0;
    while (y < y0 + h) {
      int x = x0;
      while (x < x0 + w) {
        g.translate(x, y);
        draw(draw_ops, g, jif);
        g.translate(-x, -y);
        x += tw;
      }
      y += th;
    }

    variables.put("width", width);
    variables.put("height", height);
    g.setClip(oldClip);
  }

  protected void drawTint(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    Color color = parseColor(getStringAttr(attrs, "color"));
    float alpha = Float.parseFloat(getStringAttr(attrs, "alpha"));
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }
    if (g instanceof Graphics2D) {
      Graphics2D g2 = (Graphics2D) g;
      Composite oldComp = g2.getComposite();
      AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
      g2.setComposite(ac);
      g2.setColor(color);
      g2.fillRect(x, y, w, h);
      g2.setComposite(oldComp);
    }
  }

  protected void drawTitle(Node node, Graphics g, JInternalFrame jif) {
    NamedNodeMap attrs = node.getAttributes();
    String colorStr = getStringAttr(attrs, "color");
    int i = colorStr.indexOf("gtk:fg[");
    if (i > 0) {
      colorStr = colorStr.substring(0, i) + "gtk:text[" + colorStr.substring(i + 7);
    }
    Color color = parseColor(colorStr);
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));

    String title = jif.getTitle();
    if (title != null) {
      FontMetrics fm = SwingUtilities2.getFontMetrics(jif, g);
      title = SwingUtilities2.clipStringIfNecessary(jif, fm, title,
          calculateTitleArea(jif).width);
      g.setColor(color);
      SwingUtilities2.drawString(jif, g, title, x, y + fm.getAscent());
    }
  }

  protected Dimension calculateButtonSize(JComponent titlePane) {
    int buttonHeight = getInt("button_height");
    if (buttonHeight == 0) {
      buttonHeight = titlePane.getHeight();
      if (buttonHeight == 0) {
        buttonHeight = 13;
      } else {
        Insets button_border = (Insets) frameGeometry.get("button_border");
        if (button_border != null) {
          buttonHeight -= (button_border.top + button_border.bottom);
        }
      }
    }
    int buttonWidth = getInt("button_width");
    if (buttonWidth == 0) {
      buttonWidth = buttonHeight;
      Float aspect_ratio = (Float) frameGeometry.get("aspect_ratio");
      if (aspect_ratio != null) {
        buttonWidth = (int) (buttonHeight / aspect_ratio.floatValue());
      }
    }
    return new Dimension(buttonWidth, buttonHeight);
  }

  protected Rectangle calculateTitleArea(JInternalFrame jif) {
    JComponent titlePane = findChild(jif, "InternalFrame.northPane");
    Dimension buttonDim = calculateButtonSize(titlePane);
    Insets title_border = (Insets) frameGeometry.get("title_border");
    Insets button_border = (Insets) getFrameGeometry().get("button_border");

    Rectangle r = new Rectangle();
    r.x = getInt("left_titlebar_edge");
    r.y = 0;
    r.height = titlePane.getHeight();
    if (title_border != null) {
      r.x += title_border.left;
      r.y += title_border.top;
      r.height -= (title_border.top + title_border.bottom);
    }

    if (titlePane.getParent().getComponentOrientation().isLeftToRight()) {
      r.x += buttonDim.width;
      if (button_border != null) {
        r.x += button_border.left;
      }
      r.width = titlePane.getWidth() - r.x - getInt("right_titlebar_edge");
      if (jif.isClosable()) {
        r.width -= buttonDim.width;
      }
      if (jif.isMaximizable()) {
        r.width -= buttonDim.width;
      }
      if (jif.isIconifiable()) {
        r.width -= buttonDim.width;
      }
    } else {
      if (jif.isClosable()) {
        r.x += buttonDim.width;
      }
      if (jif.isMaximizable()) {
        r.x += buttonDim.width;
      }
      if (jif.isIconifiable()) {
        r.x += buttonDim.width;
      }
      r.width = titlePane.getWidth() - r.x - getInt("right_titlebar_edge")
          - buttonDim.width;
      if (button_border != null) {
        r.x -= button_border.right;
      }
    }
    if (title_border != null) {
      r.width -= title_border.right;
    }
    return r;
  }


  protected int calculateTitleTextWidth(Graphics g, JInternalFrame jif) {
    String title = jif.getTitle();
    if (title != null) {
      Rectangle r = calculateTitleArea(jif);
      return Math.min(SwingUtilities2.stringWidth(jif,
          SwingUtilities2.getFontMetrics(jif, g), title), r.width);
    }
    return 0;
  }

  protected void setClip(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }
    if (g instanceof Graphics2D) {
      ((Graphics2D) g).clip(new Rectangle(x, y, w, h));
    }
  }

  protected void drawGTKArrow(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    String arrow = getStringAttr(attrs, "arrow");
    String shadow = getStringAttr(attrs, "shadow");
    String stateStr = getStringAttr(attrs, "state").toUpperCase();
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));

    int state = -1;
    if ("NORMAL".equals(stateStr)) {
      state = ENABLED;
    } else if ("SELECTED".equals(stateStr)) {
      state = SELECTED;
    } else if ("INSENSITIVE".equals(stateStr)) {
      state = DISABLED;
    } else if ("PRELIGHT".equals(stateStr)) {
      state = MOUSE_OVER;
    }

    ShadowType shadowType = null;
    if ("in".equals(shadow)) {
      shadowType = ShadowType.IN;
    } else if ("out".equals(shadow)) {
      shadowType = ShadowType.OUT;
    } else if ("etched_in".equals(shadow)) {
      shadowType = ShadowType.ETCHED_IN;
    } else if ("etched_out".equals(shadow)) {
      shadowType = ShadowType.ETCHED_OUT;
    } else if ("none".equals(shadow)) {
      shadowType = ShadowType.NONE;
    }

    ArrowType direction = null;
    if ("up".equals(arrow)) {
      direction = ArrowType.UP;
    } else if ("down".equals(arrow)) {
      direction = ArrowType.DOWN;
    } else if ("left".equals(arrow)) {
      direction = ArrowType.LEFT;
    } else if ("right".equals(arrow)) {
      direction = ArrowType.RIGHT;
    }

    GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
        "metacity-arrow", x, y, w, h, shadowType, direction);
  }

  protected void drawGTKBox(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    String shadow = getStringAttr(attrs, "shadow");
    String stateStr = getStringAttr(attrs, "state").toUpperCase();
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));

    int state = -1;
    if ("NORMAL".equals(stateStr)) {
      state = ENABLED;
    } else if ("SELECTED".equals(stateStr)) {
      state = SELECTED;
    } else if ("INSENSITIVE".equals(stateStr)) {
      state = DISABLED;
    } else if ("PRELIGHT".equals(stateStr)) {
      state = MOUSE_OVER;
    }

    ShadowType shadowType = null;
    if ("in".equals(shadow)) {
      shadowType = ShadowType.IN;
    } else if ("out".equals(shadow)) {
      shadowType = ShadowType.OUT;
    } else if ("etched_in".equals(shadow)) {
      shadowType = ShadowType.ETCHED_IN;
    } else if ("etched_out".equals(shadow)) {
      shadowType = ShadowType.ETCHED_OUT;
    } else if ("none".equals(shadow)) {
      shadowType = ShadowType.NONE;
    }
    GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
        "metacity-box", x, y, w, h, shadowType, null);
  }

  protected void drawGTKVLine(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    String stateStr = getStringAttr(attrs, "state").toUpperCase();

    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y1 = aee.evaluate(getStringAttr(attrs, "y1"));
    int y2 = aee.evaluate(getStringAttr(attrs, "y2"));

    int state = -1;
    if ("NORMAL".equals(stateStr)) {
      state = ENABLED;
    } else if ("SELECTED".equals(stateStr)) {
      state = SELECTED;
    } else if ("INSENSITIVE".equals(stateStr)) {
      state = DISABLED;
    } else if ("PRELIGHT".equals(stateStr)) {
      state = MOUSE_OVER;
    }

    GTKPainter.INSTANCE.paintMetacityElement(context, g, state,
        "metacity-vline", x, y1, 1, y2 - y1, null, null);
  }

  protected void drawGradient(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    String type = getStringAttr(attrs, "type");
    float alpha = getFloatAttr(node, "alpha", -1F);
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }

    // Get colors from child nodes
    Node[] colorNodes = getNodesByName(node, "color");
    Color[] colors = new Color[colorNodes.length];
    for (int i = 0; i < colorNodes.length; i++) {
      colors[i] = parseColor(getStringAttr(colorNodes[i], "value"));
    }

    boolean horizontal = ("diagonal".equals(type) || "horizontal".equals(type));
    boolean vertical = ("diagonal".equals(type) || "vertical".equals(type));

    if (g instanceof Graphics2D) {
      Graphics2D g2 = (Graphics2D) g;
      Composite oldComp = g2.getComposite();
      if (alpha >= 0F) {
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
      }
      int n = colors.length - 1;
      for (int i = 0; i < n; i++) {
        g2.setPaint(new GradientPaint(x + (horizontal ? (i * w / n) : 0),
            y + (vertical ? (i * h / n) : 0),
            colors[i],
            x + (horizontal ? ((i + 1) * w / n) : 0),
            y + (vertical ? ((i + 1) * h / n) : 0),
            colors[i + 1]));
        g2.fillRect(x + (horizontal ? (i * w / n) : 0),
            y + (vertical ? (i * h / n) : 0),
            (horizontal ? (w / n) : w),
            (vertical ? (h / n) : h));
      }
      g2.setComposite(oldComp);
    }
  }

  protected void drawImage(Node node, Graphics g) {
    NamedNodeMap attrs = node.getAttributes();
    String filename = getStringAttr(attrs, "filename");
    String colorizeStr = getStringAttr(attrs, "colorize");
    Color colorize = (colorizeStr != null) ? parseColor(colorizeStr) : null;
    String alpha = getStringAttr(attrs, "alpha");
    Image object = (colorize != null) ? getImage(filename, colorize) : getImage(filename);
    variables.put("object_width", object.getWidth(null));
    variables.put("object_height", object.getHeight(null));
    String fill_type = getStringAttr(attrs, "fill_type");
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }

    if (alpha != null) {
      if ("tile".equals(fill_type)) {
        StringTokenizer tokenizer = new StringTokenizer(alpha, ":");
        float[] alphas = new float[tokenizer.countTokens()];
        for (int i = 0; i < alphas.length; i++) {
          alphas[i] = Float.parseFloat(tokenizer.nextToken());
        }
        tileImage(g, object, x, y, w, h, alphas);
      } else {
        float a = Float.parseFloat(alpha);
        if (g instanceof Graphics2D) {
          Graphics2D g2 = (Graphics2D) g;
          Composite oldComp = g2.getComposite();
          g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
          g2.drawImage(object, x, y, w, h, null);
          g2.setComposite(oldComp);
        }
      }
    } else {
      g.drawImage(object, x, y, w, h, null);
    }
  }

  protected void drawIcon(Node node, Graphics g, JInternalFrame jif) {
    Icon icon = jif.getFrameIcon();
    if (icon == null) {
      return;
    }

    NamedNodeMap attrs = node.getAttributes();
    String alpha = getStringAttr(attrs, "alpha");
    int x = aee.evaluate(getStringAttr(attrs, "x"));
    int y = aee.evaluate(getStringAttr(attrs, "y"));
    int w = aee.evaluate(getStringAttr(attrs, "width"));
    int h = aee.evaluate(getStringAttr(attrs, "height"));
    if (getInt("width") == -1) {
      x -= w;
    }
    if (getInt("height") == -1) {
      y -= h;
    }

    if (alpha != null) {
      float a = Float.parseFloat(alpha);
      if (g instanceof Graphics2D) {
        Graphics2D g2 = (Graphics2D) g;
        Composite oldComp = g2.getComposite();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, a));
        icon.paintIcon(jif, g, x, y);
        g2.setComposite(oldComp);
      }
    } else {
      icon.paintIcon(jif, g, x, y);
    }
  }

  protected void drawInclude(Node node, Graphics g, JInternalFrame jif) {
    int oldWidth = getInt("width");
    int oldHeight = getInt("height");

    NamedNodeMap attrs = node.getAttributes();
    int x = aee.evaluate(getStringAttr(attrs, "x"), 0);
    int y = aee.evaluate(getStringAttr(attrs, "y"), 0);
    int w = aee.evaluate(getStringAttr(attrs, "width"), -1);
    int h = aee.evaluate(getStringAttr(attrs, "height"), -1);

    if (w != -1) {
      variables.put("width", w);
    }
    if (h != -1) {
      variables.put("height", h);
    }

    Node draw_ops = getNode("draw_ops", new String[]{
        "name", getStringAttr(node, "name")
    });
    g.translate(x, y);
    draw(draw_ops, g, jif);
    g.translate(-x, -y);

    if (w != -1) {
      variables.put("width", oldWidth);
    }
    if (h != -1) {
      variables.put("height", oldHeight);
    }
  }

  protected void draw(Node draw_ops, Graphics g, JInternalFrame jif) {
    if (draw_ops != null) {
      NodeList nodes = draw_ops.getChildNodes();
      if (nodes != null) {
        Shape oldClip = g.getClip();
        for (int i = 0; i < nodes.getLength(); i++) {
          Node child = nodes.item(i);
          if (child.getNodeType() == Node.ELEMENT_NODE) {
            try {
              String name = child.getNodeName();
              if ("include".equals(name)) {
                drawInclude(child, g, jif);
              } else if ("arc".equals(name)) {
                drawArc(child, g);
              } else if ("clip".equals(name)) {
                setClip(child, g);
              } else if ("gradient".equals(name)) {
                drawGradient(child, g);
              } else if ("gtk_arrow".equals(name)) {
                drawGTKArrow(child, g);
              } else if ("gtk_box".equals(name)) {
                drawGTKBox(child, g);
              } else if ("gtk_vline".equals(name)) {
                drawGTKVLine(child, g);
              } else if ("image".equals(name)) {
                drawImage(child, g);
              } else if ("icon".equals(name)) {
                drawIcon(child, g, jif);
              } else if ("line".equals(name)) {
                drawLine(child, g);
              } else if ("rectangle".equals(name)) {
                drawRectangle(child, g);
              } else if ("tint".equals(name)) {
                drawTint(child, g);
              } else if ("tile".equals(name)) {
                drawTile(child, g, jif);
              } else if ("title".equals(name)) {
                drawTitle(child, g, jif);
              } else {
                System.err.println("Unknown Metacity drawing op: " + child);
              }
            } catch (NumberFormatException ex) {
              logError(themeName, ex);
            }
          }
        }
        g.setClip(oldClip);
      }
    }
  }

  protected void drawPiece(Node frame_style, Graphics g, String position, int x, int y,
      int width, int height, JInternalFrame jif) {
    Node piece = getNode(frame_style, "piece", new String[]{"position", position});
    if (piece != null) {
      Node draw_ops;
      String draw_ops_name = getStringAttr(piece, "draw_ops");
      if (draw_ops_name != null) {
        draw_ops = getNode("draw_ops", new String[]{"name", draw_ops_name});
      } else {
        draw_ops = getNode(piece, "draw_ops", null);
      }
      variables.put("width", width);
      variables.put("height", height);
      g.translate(x, y);
      draw(draw_ops, g, jif);
      g.translate(-x, -y);
    }
  }


  Insets getBorderInsets(SynthContext context, Insets insets) {
    updateFrameGeometry(context);

    if (insets == null) {
      insets = new Insets(0, 0, 0, 0);
    }
    insets.top = ((Insets) frameGeometry.get("title_border")).top;
    insets.bottom = getInt("bottom_height");
    insets.left = getInt("left_width");
    insets.right = getInt("right_width");
    return insets;
  }


  private void updateFrameGeometry(SynthContext context) {
    this.context = context;
    JComponent comp = context.getComponent();
    JComponent titlePane = findChild(comp, "InternalFrame.northPane");

    JInternalFrame jif = null;
    if (comp instanceof JInternalFrame) {
      jif = (JInternalFrame) comp;
    } else if (comp instanceof JInternalFrame.JDesktopIcon) {
      jif = ((JInternalFrame.JDesktopIcon) comp).getInternalFrame();
    } else {
      assert false : "component is not JInternalFrame or JInternalFrame.JDesktopIcon";
      return;
    }

    if (frame_style_set == null) {
      Node window = getNode("window", new String[]{"type", "normal"});

      if (window != null) {
        frame_style_set = getNode("frame_style_set",
            new String[]{"name", getStringAttr(window, "style_set")});
      }

      if (frame_style_set == null) {
        frame_style_set = getNode("frame_style_set", new String[]{"name", "normal"});
      }
    }

    if (frame_style_set != null) {
      Node frame = getNode(frame_style_set, "frame", new String[]{
          "focus", (jif.isSelected() ? "yes" : "no"),
          "state", (jif.isMaximum() ? "maximized" : "normal")
      });

      if (frame != null) {
        Node frame_style = getNode("frame_style", new String[]{
            "name", getStringAttr(frame, "style")
        });
        if (frame_style != null) {
          Map gm = frameGeometries.get(getStringAttr(frame_style, "geometry"));

          setFrameGeometry(titlePane, gm);
        }
      }
    }
  }


  protected static void logError(String themeName, Exception ex) {
    logError(themeName, ex.toString());
  }

  protected static void logError(String themeName, String msg) {
    if (!errorLogged) {
      System.err.println("Exception in Metacity for theme \"" + themeName + "\": " + msg);
      errorLogged = true;
    }
  }

  // XML Parsing


  protected static Document getXMLDoc(final URL xmlFile)
      throws IOException,
      ParserConfigurationException,
      SAXException {
    if (documentBuilder == null) {
      documentBuilder =
          DocumentBuilderFactory.newInstance().newDocumentBuilder();
    }
    InputStream inputStream =
        AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
          public InputStream run() {
            try {
              return new BufferedInputStream(xmlFile.openStream());
            } catch (IOException ex) {
              return null;
            }
          }
        });

    Document doc = null;
    if (inputStream != null) {
      doc = documentBuilder.parse(inputStream);
    }
    return doc;
  }


  protected Node[] getNodesByName(Node parent, String name) {
    NodeList nodes = parent.getChildNodes(); // ElementNode
    int n = nodes.getLength();
    ArrayList<Node> list = new ArrayList<Node>();
    for (int i = 0; i < n; i++) {
      Node node = nodes.item(i);
      if (name.equals(node.getNodeName())) {
        list.add(node);
      }
    }
    return list.toArray(new Node[list.size()]);
  }


  protected Node getNode(String tagName, String[] attrs) {
    NodeList nodes = xmlDoc.getElementsByTagName(tagName);
    return (nodes != null) ? getNode(nodes, tagName, attrs) : null;
  }

  protected Node getNode(Node parent, String name, String[] attrs) {
    Node node = null;
    NodeList nodes = parent.getChildNodes();
    if (nodes != null) {
      node = getNode(nodes, name, attrs);
    }
    if (node == null) {
      String inheritFrom = getStringAttr(parent, "parent");
      if (inheritFrom != null) {
        Node inheritFromNode = getNode(parent.getParentNode(),
            parent.getNodeName(),
            new String[]{"name", inheritFrom});
        if (inheritFromNode != null) {
          node = getNode(inheritFromNode, name, attrs);
        }
      }
    }
    return node;
  }

  protected Node getNode(NodeList nodes, String name, String[] attrs) {
    int n = nodes.getLength();
    for (int i = 0; i < n; i++) {
      Node node = nodes.item(i);
      if (name.equals(node.getNodeName())) {
        if (attrs != null) {
          NamedNodeMap nodeAttrs = node.getAttributes();
          if (nodeAttrs != null) {
            boolean matches = true;
            int nAttrs = attrs.length / 2;
            for (int a = 0; a < nAttrs; a++) {
              String aName = attrs[a * 2];
              String aValue = attrs[a * 2 + 1];
              Node attr = nodeAttrs.getNamedItem(aName);
              if (attr == null ||
                  aValue != null && !aValue.equals(attr.getNodeValue())) {
                matches = false;
                break;
              }
            }
            if (matches) {
              return node;
            }
          }
        } else {
          return node;
        }
      }
    }
    return null;
  }

  protected String getStringAttr(Node node, String name) {
    String value = null;
    NamedNodeMap attrs = node.getAttributes();
    if (attrs != null) {
      value = getStringAttr(attrs, name);
      if (value == null) {
        String inheritFrom = getStringAttr(attrs, "parent");
        if (inheritFrom != null) {
          Node inheritFromNode = getNode(node.getParentNode(),
              node.getNodeName(),
              new String[]{"name", inheritFrom});
          if (inheritFromNode != null) {
            value = getStringAttr(inheritFromNode, name);
          }
        }
      }
    }
    return value;
  }

  protected String getStringAttr(NamedNodeMap attrs, String name) {
    Node item = attrs.getNamedItem(name);
    return (item != null) ? item.getNodeValue() : null;
  }

  protected boolean getBooleanAttr(Node node, String name, boolean fallback) {
    String str = getStringAttr(node, name);
    if (str != null) {
      return Boolean.valueOf(str).booleanValue();
    }
    return fallback;
  }

  protected int getIntAttr(Node node, String name, int fallback) {
    String str = getStringAttr(node, name);
    int value = fallback;
    if (str != null) {
      try {
        value = Integer.parseInt(str);
      } catch (NumberFormatException ex) {
        logError(themeName, ex);
      }
    }
    return value;
  }

  protected float getFloatAttr(Node node, String name, float fallback) {
    String str = getStringAttr(node, name);
    float value = fallback;
    if (str != null) {
      try {
        value = Float.parseFloat(str);
      } catch (NumberFormatException ex) {
        logError(themeName, ex);
      }
    }
    return value;
  }


  protected Color parseColor(String str) {
    StringTokenizer tokenizer = new StringTokenizer(str, "/");
    int n = tokenizer.countTokens();
    if (n > 1) {
      String function = tokenizer.nextToken();
      if ("shade".equals(function)) {
        assert (n == 3);
        Color c = parseColor2(tokenizer.nextToken());
        float alpha = Float.parseFloat(tokenizer.nextToken());
        return GTKColorType.adjustColor(c, 1.0F, alpha, alpha);
      } else if ("blend".equals(function)) {
        assert (n == 4);
        Color bg = parseColor2(tokenizer.nextToken());
        Color fg = parseColor2(tokenizer.nextToken());
        float alpha = Float.parseFloat(tokenizer.nextToken());
        if (alpha > 1.0f) {
          alpha = 1.0f / alpha;
        }

        return new Color((int) (bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
            (int) (bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)),
            (int) (bg.getRed() + ((fg.getRed() - bg.getRed()) * alpha)));
      } else {
        System.err.println("Unknown Metacity color function=" + str);
        return null;
      }
    } else {
      return parseColor2(str);
    }
  }

  protected Color parseColor2(String str) {
    Color c = null;
    if (str.startsWith("gtk:")) {
      int i1 = str.indexOf('[');
      if (i1 > 3) {
        String typeStr = str.substring(4, i1).toLowerCase();
        int i2 = str.indexOf(']');
        if (i2 > i1 + 1) {
          String stateStr = str.substring(i1 + 1, i2).toUpperCase();
          int state = -1;
          if ("ACTIVE".equals(stateStr)) {
            state = PRESSED;
          } else if ("INSENSITIVE".equals(stateStr)) {
            state = DISABLED;
          } else if ("NORMAL".equals(stateStr)) {
            state = ENABLED;
          } else if ("PRELIGHT".equals(stateStr)) {
            state = MOUSE_OVER;
          } else if ("SELECTED".equals(stateStr)) {
            state = SELECTED;
          }
          ColorType type = null;
          if ("fg".equals(typeStr)) {
            type = GTKColorType.FOREGROUND;
          } else if ("bg".equals(typeStr)) {
            type = GTKColorType.BACKGROUND;
          } else if ("base".equals(typeStr)) {
            type = GTKColorType.TEXT_BACKGROUND;
          } else if ("text".equals(typeStr)) {
            type = GTKColorType.TEXT_FOREGROUND;
          } else if ("dark".equals(typeStr)) {
            type = GTKColorType.DARK;
          } else if ("light".equals(typeStr)) {
            type = GTKColorType.LIGHT;
          }
          if (state >= 0 && type != null) {
            c = ((GTKStyle) context.getStyle()).getGTKColor(context, state, type);
          }
        }
      }
    }
    if (c == null) {
      c = parseColorString(str);
    }
    return c;
  }

  private static Color parseColorString(String str) {
    if (str.charAt(0) == '#') {
      str = str.substring(1);

      int i = str.length();

      if (i < 3 || i > 12 || (i % 3) != 0) {
        return null;
      }

      i /= 3;

      int r;
      int g;
      int b;

      try {
        r = Integer.parseInt(str.substring(0, i), 16);
        g = Integer.parseInt(str.substring(i, i * 2), 16);
        b = Integer.parseInt(str.substring(i * 2, i * 3), 16);
      } catch (NumberFormatException nfe) {
        return null;
      }

      if (i == 4) {
        return new ColorUIResource(r / 65535.0f, g / 65535.0f, b / 65535.0f);
      } else if (i == 1) {
        return new ColorUIResource(r / 15.0f, g / 15.0f, b / 15.0f);
      } else if (i == 2) {
        return new ColorUIResource(r, g, b);
      } else {
        return new ColorUIResource(r / 4095.0f, g / 4095.0f, b / 4095.0f);
      }
    } else {
      return XColors.lookupColor(str);
    }
  }

  class ArithmeticExpressionEvaluator {

    private PeekableStringTokenizer tokenizer;

    int evaluate(String expr) {
      tokenizer = new PeekableStringTokenizer(expr, " \t+-*/%()", true);
      return Math.round(expression());
    }

    int evaluate(String expr, int fallback) {
      return (expr != null) ? evaluate(expr) : fallback;
    }

    public float expression() {
      float value = getTermValue();
      boolean done = false;
      while (!done && tokenizer.hasMoreTokens()) {
        String next = tokenizer.peek();
        if ("+".equals(next) ||
            "-".equals(next) ||
            "`max`".equals(next) ||
            "`min`".equals(next)) {
          tokenizer.nextToken();
          float value2 = getTermValue();
          if ("+".equals(next)) {
            value += value2;
          } else if ("-".equals(next)) {
            value -= value2;
          } else if ("`max`".equals(next)) {
            value = Math.max(value, value2);
          } else if ("`min`".equals(next)) {
            value = Math.min(value, value2);
          }
        } else {
          done = true;
        }
      }
      return value;
    }

    public float getTermValue() {
      float value = getFactorValue();
      boolean done = false;
      while (!done && tokenizer.hasMoreTokens()) {
        String next = tokenizer.peek();
        if ("*".equals(next) || "/".equals(next) || "%".equals(next)) {
          tokenizer.nextToken();
          float value2 = getFactorValue();
          if ("*".equals(next)) {
            value *= value2;
          } else if ("/".equals(next)) {
            value /= value2;
          } else {
            value %= value2;
          }
        } else {
          done = true;
        }
      }
      return value;
    }

    public float getFactorValue() {
      float value;
      if ("(".equals(tokenizer.peek())) {
        tokenizer.nextToken();
        value = expression();
        tokenizer.nextToken(); // skip right paren
      } else {
        String token = tokenizer.nextToken();
        if (Character.isDigit(token.charAt(0))) {
          value = Float.parseFloat(token);
        } else {
          Integer i = variables.get(token);
          if (i == null) {
            i = (Integer) getFrameGeometry().get(token);
          }
          if (i == null) {
            logError(themeName, "Variable \"" + token + "\" not defined");
            return 0;
          }
          value = (i != null) ? i.intValue() : 0F;
        }
      }
      return value;
    }


  }

  static class PeekableStringTokenizer extends StringTokenizer {

    String token = null;

    public PeekableStringTokenizer(String str, String delim,
        boolean returnDelims) {
      super(str, delim, returnDelims);
      peek();
    }

    public String peek() {
      if (token == null) {
        token = nextToken();
      }
      return token;
    }

    public boolean hasMoreTokens() {
      return (token != null || super.hasMoreTokens());
    }

    public String nextToken() {
      if (token != null) {
        String t = token;
        token = null;
        if (hasMoreTokens()) {
          peek();
        }
        return t;
      } else {
        String token = super.nextToken();
        while ((token.equals(" ") || token.equals("\t"))
            && hasMoreTokens()) {
          token = super.nextToken();
        }
        return token;
      }
    }
  }


  static class RoundRectClipShape extends RectangularShape {

    static final int TOP_LEFT = 1;
    static final int TOP_RIGHT = 2;
    static final int BOTTOM_LEFT = 4;
    static final int BOTTOM_RIGHT = 8;

    int x;
    int y;
    int width;
    int height;
    int arcwidth;
    int archeight;
    int corners;

    public RoundRectClipShape() {
    }

    public RoundRectClipShape(int x, int y, int w, int h,
        int arcw, int arch, int corners) {
      setRoundedRect(x, y, w, h, arcw, arch, corners);
    }

    public void setRoundedRect(int x, int y, int w, int h,
        int arcw, int arch, int corners) {
      this.corners = corners;
      this.x = x;
      this.y = y;
      this.width = w;
      this.height = h;
      this.arcwidth = arcw;
      this.archeight = arch;
    }

    public double getX() {
      return (double) x;
    }

    public double getY() {
      return (double) y;
    }

    public double getWidth() {
      return (double) width;
    }

    public double getHeight() {
      return (double) height;
    }

    public double getArcWidth() {
      return (double) arcwidth;
    }

    public double getArcHeight() {
      return (double) archeight;
    }

    public boolean isEmpty() {
      return false;  // Not called
    }

    public Rectangle2D getBounds2D() {
      return null;  // Not called
    }

    public int getCornerFlags() {
      return corners;
    }

    public void setFrame(double x, double y, double w, double h) {
      // Not called
    }

    public boolean contains(double x, double y) {
      return false;  // Not called
    }

    private int classify(double coord, double left, double right, double arcsize) {
      return 0;  // Not called
    }

    public boolean intersects(double x, double y, double w, double h) {
      return false;  // Not called
    }

    public boolean contains(double x, double y, double w, double h) {
      return false;  // Not called
    }

    public PathIterator getPathIterator(AffineTransform at) {
      return new RoundishRectIterator(this, at);
    }


    static class RoundishRectIterator implements PathIterator {

      double x, y, w, h, aw, ah;
      AffineTransform affine;
      int index;

      double ctrlpts[][];
      int types[];

      private static final double angle = Math.PI / 4.0;
      private static final double a = 1.0 - Math.cos(angle);
      private static final double b = Math.tan(angle);
      private static final double c = Math.sqrt(1.0 + b * b) - 1 + a;
      private static final double cv = 4.0 / 3.0 * a * b / c;
      private static final double acv = (1.0 - cv) / 2.0;

      // For each array:
      //     4 values for each point {v0, v1, v2, v3}:
      //         point = (x + v0 * w + v1 * arcWidth,
      //                  y + v2 * h + v3 * arcHeight);
      private static final double CtrlPtTemplate[][] = {
          {0.0, 0.0, 1.0, 0.0},     /* BOTTOM LEFT corner */
          {0.0, 0.0, 1.0, -0.5},     /* BOTTOM LEFT arc start */
          {0.0, 0.0, 1.0, -acv,       /* BOTTOM LEFT arc curve */
              0.0, acv, 1.0, 0.0,
              0.0, 0.5, 1.0, 0.0},
          {1.0, 0.0, 1.0, 0.0},     /* BOTTOM RIGHT corner */
          {1.0, -0.5, 1.0, 0.0},     /* BOTTOM RIGHT arc start */
          {1.0, -acv, 1.0, 0.0,       /* BOTTOM RIGHT arc curve */
              1.0, 0.0, 1.0, -acv,
              1.0, 0.0, 1.0, -0.5},
          {1.0, 0.0, 0.0, 0.0},     /* TOP RIGHT corner */
          {1.0, 0.0, 0.0, 0.5},     /* TOP RIGHT arc start */
          {1.0, 0.0, 0.0, acv,       /* TOP RIGHT arc curve */
              1.0, -acv, 0.0, 0.0,
              1.0, -0.5, 0.0, 0.0},
          {0.0, 0.0, 0.0, 0.0},     /* TOP LEFT corner */
          {0.0, 0.5, 0.0, 0.0},     /* TOP LEFT arc start */
          {0.0, acv, 0.0, 0.0,       /* TOP LEFT arc curve */
              0.0, 0.0, 0.0, acv,
              0.0, 0.0, 0.0, 0.5},
          {},                             /* Closing path element */
      };
      private static final int CornerFlags[] = {
          RoundRectClipShape.BOTTOM_LEFT,
          RoundRectClipShape.BOTTOM_RIGHT,
          RoundRectClipShape.TOP_RIGHT,
          RoundRectClipShape.TOP_LEFT,
      };

      RoundishRectIterator(RoundRectClipShape rr, AffineTransform at) {
        this.x = rr.getX();
        this.y = rr.getY();
        this.w = rr.getWidth();
        this.h = rr.getHeight();
        this.aw = Math.min(w, Math.abs(rr.getArcWidth()));
        this.ah = Math.min(h, Math.abs(rr.getArcHeight()));
        this.affine = at;
        if (w < 0 || h < 0) {
          // Don't draw anything...
          ctrlpts = new double[0][];
          types = new int[0];
        } else {
          int corners = rr.getCornerFlags();
          int numedges = 5;  // 4xCORNER_POINT, CLOSE
          for (int i = 1; i < 0x10; i <<= 1) {
            // Add one for each corner that has a curve
            if ((corners & i) != 0) {
              numedges++;
            }
          }
          ctrlpts = new double[numedges][];
          types = new int[numedges];
          int j = 0;
          for (int i = 0; i < 4; i++) {
            types[j] = SEG_LINETO;
            if ((corners & CornerFlags[i]) == 0) {
              ctrlpts[j++] = CtrlPtTemplate[i * 3 + 0];
            } else {
              ctrlpts[j++] = CtrlPtTemplate[i * 3 + 1];
              types[j] = SEG_CUBICTO;
              ctrlpts[j++] = CtrlPtTemplate[i * 3 + 2];
            }
          }
          types[j] = SEG_CLOSE;
          ctrlpts[j++] = CtrlPtTemplate[12];
          types[0] = SEG_MOVETO;
        }
      }

      public int getWindingRule() {
        return WIND_NON_ZERO;
      }

      public boolean isDone() {
        return index >= ctrlpts.length;
      }

      public void next() {
        index++;
      }

      public int currentSegment(float[] coords) {
        if (isDone()) {
          throw new NoSuchElementException("roundrect iterator out of bounds");
        }
        double ctrls[] = ctrlpts[index];
        int nc = 0;
        for (int i = 0; i < ctrls.length; i += 4) {
          coords[nc++] = (float) (x + ctrls[i + 0] * w + ctrls[i + 1] * aw);
          coords[nc++] = (float) (y + ctrls[i + 2] * h + ctrls[i + 3] * ah);
        }
        if (affine != null) {
          affine.transform(coords, 0, coords, 0, nc / 2);
        }
        return types[index];
      }

      public int currentSegment(double[] coords) {
        if (isDone()) {
          throw new NoSuchElementException("roundrect iterator out of bounds");
        }
        double ctrls[] = ctrlpts[index];
        int nc = 0;
        for (int i = 0; i < ctrls.length; i += 4) {
          coords[nc++] = x + ctrls[i + 0] * w + ctrls[i + 1] * aw;
          coords[nc++] = y + ctrls[i + 2] * h + ctrls[i + 3] * ah;
        }
        if (affine != null) {
          affine.transform(coords, 0, coords, 0, nc / 2);
        }
        return types[index];
      }
    }
  }
}
